﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using Baddies.Utils;
using BaddiesEditor.CustomComponent;
using BaddiesEditor.Events;
using BaddiesEditor.PaintTools;

namespace BaddiesEditor
{
    /// <summary>
    /// Main form for the game editor.
    /// </summary>
    public partial class EditorForm : Form
    {
        /// <summary>
        /// Reference to the last added tile to the tilePanel, for dynamic
        /// positioning of new tiles.
        /// </summary>
        private PictureBox lastAddedTile;

        /// <summary>
        /// Margin to be observed when adding a new tile to the tilePanel.
        /// </summary>
        private int tileMargin = 2;

        /// <summary>
        /// Bitmap with the current loaded tileset.
        /// </summary>
        private Bitmap tileset;

        /// <summary>
        /// Reference to the game running in the PictureBox.
        /// </summary>
        private EditorGame game;

        /// <summary>
        /// Reference to the map project.
        /// </summary>
        private MapProject mapProject;

        /// <summary>
        /// Keep track of the mouse coords while on the gameViewport.
        /// </summary>
        private Point mouseCoords;

        /// <summary>
        /// Keeps track of whether the mouse in the gameViewport is clicked.
        /// </summary>
        private bool mouseRightClicked;

        /// <summary>
        /// Position in the tileSet of the last tile clicked by the user.
        /// </summary>
        private Point lastClickedTilePos;

        /// <summary>
        /// Indicates whether the user has a tile clicked.
        /// </summary>
        private bool tileClicked;

        /// <summary>
        /// Current selected paint tool.
        /// </summary>
        private PaintTool currentPaintTool;

        /// <summary>
        /// Sets the reference to the game.
        /// </summary>
        /// <value>Game to reference.</value>
        public EditorGame Game
        {
            set { this.game = value; }
        }

        /// <summary>
        /// Sets the mapProject reference.
        /// </summary>
        /// <value>Reference to the mapProject.</value>
        public MapProject MapProject
        {
            set { this.mapProject = value; }
        }

        /// <summary>
        /// Information needed to change the icon of the cursor.
        /// </summary>
        /// <see cref="http://www.switchonthecode.com/tutorials/csharp-tutorial-how-to-use-custom-cursors"/>
        public struct IconInfo
        {
            /// <summary>
            /// Indicates whether it's a cursor or a regular icon. False it it's a cursor.
            /// </summary>
            public bool fIcon;

            /// <summary>
            /// Click point of  the cursor. To select what is the part we click with. X Component.
            /// </summary>
            public int xHotspot;

            /// <summary>
            /// Click point of  the cursor. To select what is the part we click with. Y Component.
            /// </summary>
            public int yHotspot;

            /// <summary>
            /// Mask of the icon.
            /// </summary>
            public IntPtr hbmMask;

            /// <summary>
            /// Color of the icon.
            /// </summary>
            public IntPtr hbmColor;
        }

        /// <summary>
        /// Initializes a new instance of the EditorForm class.
        /// </summary>
        public EditorForm()
        {
            this.InitializeComponent();
            this.lastAddedTile = null;
            this.tileset = null;
            this.mouseRightClicked = false;
            this.mouseCoords = new Point();
            this.tileClicked = false;
            this.currentPaintTool = new BrushPaintTool(new Bitmap(this.BrushTool.Image, new Size(16, 16)));
        }

        /// <summary>
        /// Gets the draw surface of the game viewport to link with the xna rendering.
        /// </summary>
        /// <returns>Handler to the picturebox.</returns>
        public IntPtr GetDrawSurface()
        {
            return this.gameViewport.Handle;
        }

        /// <summary>
        /// Called on form load. Does form-specific loading.
        /// </summary>
        /// <param name="sender">Class that triggers the message.</param>
        /// <param name="e">Arguments of the message.</param>
        private void EditorForm_Load(object sender, EventArgs e)
        {
            Log.F.WriteLine("::LOG::");
        }

        /// <summary>
        /// Resizes the game GraphicsDeviceManager to fit exactly in the picturebox.
        /// </summary>
        public void ResizeGame()
        {
            this.game.SetGameSize(this.gameViewport.Width, this.gameViewport.Height); 
        }

        /// <summary>
        /// Invoked when a tile is clicked in the editor.
        /// </summary>
        /// <param name="sender">Class that sends the msg.</param>
        /// <param name="e">TileClickArg arguments.</param>
        private void TileClicked(object sender, EventArgs e)
        {
            Bitmap bmp = ((TileClickArg)e).TileBmp;
            
            this.SelectedTile.Image = bmp;
            this.gameViewport.Cursor = CreateCursor(this.currentPaintTool.CursorIcon, 0, 0);
            this.tileClicked = true;
            this.lastClickedTilePos = ((TileClickArg)e).TileSetPos;
        }

        #region LoadTiles
        /// <summary>
        /// Adds a tile to the tileset box, by cropping the bitmap and setting it in a picture box.
        /// </summary>
        /// <param name="bitmap">Bitmap to get the tile image from.</param>
        /// <param name="tilemapCoords">Coords in the bitmap for the image.</param>
        private void AddTile(Bitmap bitmap, Point tilemapCoords)
        {
            int tileSize = this.mapProject.TileSize;
            TilePictureBox pic = new TilePictureBox(tilemapCoords, this.TileClicked);
            pic.Size = new Size(tileSize, tileSize);
            pic.Image = bitmap;
            this.tilePanel.Controls.Add(pic);

            // Position it
            if (this.lastAddedTile == null)
            {
                // If there is no "Last added tile" we're the first, 
                // so go to the initial location.
                pic.Location = new Point(this.tileMargin, this.tileMargin);
            }
            else
            {
                // Get the space left to the left of the last added 
                int curLeftMargin = this.lastAddedTile.Location.X + this.lastAddedTile.Size.Width;
                int maxLeftMargin = this.tilePanel.Size.Width;
                int remainingSpace = maxLeftMargin - curLeftMargin;

                // See if we fit in there
                if (remainingSpace >= pic.Size.Width + this.tileMargin)
                {
                    // If we fit, get in there!
                    pic.Location = new Point(curLeftMargin + this.tileMargin, this.lastAddedTile.Location.Y);
                }
                else
                {
                    // If we don't, jump to the next line
                    pic.Location = new Point(this.tileMargin, this.lastAddedTile.Location.Y + this.tileMargin + tileSize);
                }
            }

            this.lastAddedTile = pic;
        }

        /// <summary>
        /// Given a bitmap, it generates tiles for the tile panel control by "cutting" the bitmap in tiles of a specific size.
        /// </summary>
        /// <param name="tileMap">Bitmap to cut the tiles from.</param>
        /// <seealso cref="MapProject.TileSize"/>
        private void LoadTilesFromTileMap(Bitmap tileMap)
        {
            this.tileset = tileMap;
            this.tilePanel.Controls.Clear();
            this.lastAddedTile = null;

            for (int x = 0; x < this.tileset.Size.Width; x += this.mapProject.TileSize)
            {
                for (int y = 0; y < this.tileset.Size.Height; y += this.mapProject.TileSize)
                {
                    Rectangle cropArea = new Rectangle(x, y, this.mapProject.TileSize, this.mapProject.TileSize);
                    Bitmap tileImg = this.tileset.Clone(cropArea, this.tileset.PixelFormat);
                    this.AddTile(tileImg, new Point(x, y));
                }
            }
        }

        /// <summary>
        /// Given the filename of the bitmap to use, it loads it and uses it to generate all the tiles
        /// in the tilepanel.
        /// </summary>
        /// <param name="tileMapFileName">
        /// Bitmap url to load. The width and height of this 
        /// bitmap should be divisible by MapProject.TileSize.
        /// </param>
        public void LoadTilesFromTileMap(string tileMapFileName)
        {
            Bitmap newTileSet = new Bitmap(tileMapFileName);

            // Check the tileset is actually of tiles of the required size
            if (newTileSet.Width % this.mapProject.TileSize == 0 &&
                newTileSet.Height % this.mapProject.TileSize == 0)
            {
                this.LoadTilesFromTileMap(newTileSet);
                this.game.SetTileset(Path.GetFileNameWithoutExtension(tileMapFileName), newTileSet);
            }
            else
            {
                this.ErrorForm("The tileset image you have selected is not made of tiles of the same size as the map ( " + this.mapProject.TileSize + " pixels)");
            }
        }

        /// <summary>
        /// Called when the user clicks the menu button for creating a new tileset.
        /// </summary>
        /// <param name="sender">Class that calls the handler.</param>
        /// <param name="e">Arguments of the call.</param>
        private void LoadNewTilesetToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (this.mapProject.Initialized())
            {
                using (OpenFileDialog dlg = new OpenFileDialog())
                {
                    dlg.Title = "Open Image";
                    dlg.Filter = "image files (*.png)|*.png";

                    if (dlg.ShowDialog() == DialogResult.OK)
                    {
                        this.LoadTilesFromTileMap(dlg.FileName);
                        this.mapProject.TileMapImageFileName = dlg.FileName;
                    }
                }
            }
            else
            {
                this.ErrorForm("Create a map first");
            }
        }
        #endregion // LoadTiles

        #region ChangeCursor
        /// <summary>
        /// Gets the information from an icon.
        /// </summary>
        /// <param name="hIcon">The icon to query.</param>
        /// <param name="pIconInfo">The result is a tuple of (fIcon, xHotspot, yHotspot, hbmMask, hbmColor) The hbmMask and hbmColor items are bitmaps created for the caller, so must be freed.</param>
        /// <returns>Whether it managed to load the icon info. I think.</returns>
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool GetIconInfo(IntPtr hIcon, ref IconInfo pIconInfo);

        /// <summary>
        /// Creates a pointer for the new cursor icon.
        /// </summary>
        /// <param name="icon">The information for the icon.</param>
        /// <returns>A pointer to the new cursor icon.</returns>
        [DllImport("user32.dll")]
        public static extern IntPtr CreateIconIndirect(ref IconInfo icon);

        /// <summary>
        /// Generates a new icon and sets it as the cursor.
        /// </summary>
        /// <param name="bmp">Image for the icon.</param>
        /// <param name="xHotSpot">Click point of  the cursor. To select what is the part we click with. X Component.</param>
        /// <param name="yHotSpot">Click point of  the cursor. To select what is the part we click with. Y Component.</param>
        /// <returns>The new cursor.</returns>
        public static Cursor CreateCursor(Bitmap bmp, int xHotSpot, int yHotSpot)
        {
            IntPtr ptr = bmp.GetHicon();
            IconInfo tmp = new IconInfo();
            GetIconInfo(ptr, ref tmp);
            tmp.xHotspot = xHotSpot;
            tmp.yHotspot = yHotSpot;
            tmp.fIcon = false;
            ptr = CreateIconIndirect(ref tmp);
            return new Cursor(ptr);
        }

        /// <summary>
        /// Gets the default cursor.
        /// </summary>
        /// <returns>The arrow cursor.</returns>
        public Cursor GetDefaultCursor()
        {
            return Cursors.Arrow;
        }
        #endregion // ChangeCursor

        #region NewMap
        /// <summary>
        /// Called when the user clicks to create a new project.
        /// </summary>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        private void NewToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (this.mapProject.Initialized())
            {
                WarningForm warning = new WarningForm(
                    new EventHandler(delegate(object o, EventArgs a) { this.CreateNewMap(); }),
                    null);

                warning.SetWarningMsg(
                    "All unsaved progress in the " + 
                    this.mapProject.Name + 
                    " map will be lost. Are you sure you want to create a new map?");

                warning.Show();
            }
            else
            {
                this.CreateNewMap();
            }
        }

        /// <summary>
        /// Called when the tool menu requests a new map dialog.
        /// </summary>
        public void CreateNewMap()
        {
            NewMap newMap = new NewMap(this.NewMapOkHandler);
            newMap.Show();
        }

        /// <summary>
        /// Called when the user confirms the new map dialog.
        /// </summary>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        public void NewMapOkHandler(object sender, EventArgs e)
        { 
            NewMapConfirmArg arg = (NewMapConfirmArg)e;

            // Clear the old tileset
            this.tilePanel.Controls.Clear();

            // Generate a new map project.
            this.mapProject.Clear();
            this.mapProject.TileSize = arg.TileSize;
            this.mapProject.Name = arg.Name;

            // Change editor title
            this.Text = "Baddies Editor :: " + this.mapProject.Name;

            // Generate Baddies tile map
            this.game.NewMap(arg.Width, arg.Height, arg.TileSize);
        }
        #endregion // NewMap

        #region MouseHandle
        /// <summary>
        /// Called when the user clicks the mouse on the map picture box.
        /// </summary>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        private void GameViewport_MouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Right)
            {
                this.mouseCoords = new Point(e.X, e.Y);
                this.mouseRightClicked = true;
            }

            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                if (this.tileClicked)
                {
                    this.currentPaintTool.Initialize(e.X, e.Y, this.lastClickedTilePos.X, this.lastClickedTilePos.Y);
                    this.game.HandleMapPaint(this.currentPaintTool);
                }
            }
        }

        /// <summary>
        /// Called when the mouse is moved in the game picture box.
        /// </summary>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        private void GameViewport_MouseMove(object sender, MouseEventArgs e)
        {
            if (this.mouseRightClicked)
            {
                this.game.MoveCam(this.mouseCoords.X - e.X, this.mouseCoords.Y - e.Y);
                this.mouseCoords.X = e.X;
                this.mouseCoords.Y = e.Y;
            }
        }

        /// <summary>
        /// Called when the mouse leaves the game picturebox.
        /// </summary>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        private void GameViewport_MouseLeave(object sender, EventArgs e)
        {
            this.mouseRightClicked = false;
        }

        /// <summary>
        /// Called when the mouse button is unclicked in the game picture box.
        /// </summary>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        private void GameViewport_MouseUp(object sender, MouseEventArgs e)
        {
            this.mouseRightClicked = false;
        }

        #endregion // MouseHandle

        /// <summary>
        /// Called when the user clicks the menu to save the project.
        /// </summary>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        private void SaveToolStripMenuItem_Click(object sender, EventArgs e)
        {
            // Check we have map project to save
            if (this.mapProject.Initialized())
            {
                // If so, launch the SaveMap dialog
                SaveForm saveForm = new SaveForm(
                    this.mapProject.Name,
                    new EventHandler(delegate(object o, EventArgs arg) { this.mapProject.Save(); }));

                saveForm.Show();
            }
            else
            {
                // If not, launch an error
                this.ErrorForm("No map to save! Create a new map first");
            }           
        }

        /// <summary>
        /// Creates and displays a new error form.
        /// </summary>
        /// <param name="msg">Message to write.</param>
        public void ErrorForm(string msg)
        {
            ErrorForm error = new ErrorForm();
            error.SetErrorMsg(msg);
            error.Show();
        }

        /// <summary>
        /// Called when the user clicks the menu to create a new map project.
        /// </summary>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        private void LoadToolStripMenuItem_Click(object sender, EventArgs e)
        {
            using (OpenFileDialog dlg = new OpenFileDialog())
            {
                dlg.Title = "Open Image";
                dlg.Filter = "Map project files (*" + this.mapProject.MapProjectEnding + ")|*" + this.mapProject.MapProjectEnding;

                if (dlg.ShowDialog() == DialogResult.OK)
                {
                    this.mapProject.Load(dlg.FileName);
                }
            }            
        }

        /// <summary>
        /// Called when the user shuts donw the map project with the "X" at the top right.        
        /// </summary>
        /// <remarks>
        /// It's important to be called otherwise the xna game doesn't shut 
        /// down and keeps running in the background. Also, it closes the log, 
        /// so if it's not called, the log will be blank.
        /// </remarks>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        private void EditorForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            this.game.ShutDown();
            Log.F.Close();
        }

        /// <summary>
        /// Called when the user selects the brush tool.
        /// </summary>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        private void BrushTool_Click(object sender, EventArgs e)
        {
            this.currentPaintTool = new BrushPaintTool(new Bitmap(this.BrushTool.Image, new Size(16, 16)));
        }

        /// <summary>
        /// Called when the user selects the bucket tool.
        /// </summary>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        private void BucketTool_Click(object sender, EventArgs e)
        {
            this.currentPaintTool = new BucketPaintTool(new Bitmap(this.BucketTool.Image, new Size(16, 16)));
        }

        /// <summary>
        /// Called when the user clicks to edit the map size (number of rows / columns).
        /// </summary>
        /// <param name="sender"> Class that calls the handler.</param>
        /// <param name="e"> Arguments of the call.</param>
        private void MapSizeToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (!this.mapProject.Initialized())
            {
                this.ErrorForm("Create a new map first.");
            }
            else
            {
                EditMapSize editMapSizeForm = new EditMapSize(
                     new EventHandler(delegate(object o, EventArgs arg) { this.game.EditMapSize((EditMapSizeArg)arg); }));
                editMapSizeForm.Show();
            }
        }
    }
}
